using System;
using System.Collections.Generic;
using JetBrains.Application;
using JetBrains.Diagnostics;
using JetBrains.ReSharper.Plugins.Unity.Yaml.Psi.DeferredCaches.AssetHierarchy.Elements;
using JetBrains.ReSharper.Plugins.Unity.Yaml.Psi.DeferredCaches.AssetHierarchy.Elements.Prefabs;
using JetBrains.ReSharper.Plugins.Unity.Yaml.Psi.DeferredCaches.AssetHierarchy.Elements.Stripped;
using JetBrains.ReSharper.Plugins.Unity.Yaml.Psi.DeferredCaches.AssetHierarchy.References;
using JetBrains.ReSharper.Psi;
using JetBrains.Util;

namespace JetBrains.ReSharper.Plugins.Unity.Yaml.Psi.DeferredCaches.AssetHierarchy
{
    public partial class AssetDocumentHierarchyElement : IUnityAssetDataElement
    {

        // for avoiding boxing (use less memory) we are stroing popular elements in lists with specified type
        private readonly List<IHierarchyElement> myOtherBoxedElements;
        
        // Prefab instances' modifications points to locations which are not existing. 
        // To simplify our inner logic, we will create stripped hierarchy element for each component/gameobject reference (m_Target)
        // in prefab modifications
        private readonly List<IHierarchyElement> myOtherFakeStrippedElements;
        
        // avoid boxing
        private readonly List<TransformHierarchy> myTransformElements;
        private readonly List<ScriptComponentHierarchy> myScriptComponentElements;
        private readonly List<ComponentHierarchy> myComponentElements;
        private readonly List<GameObjectHierarchy> myGameObjectHierarchies;

        private readonly Dictionary<long, int> myGameObjectLocationToTransform; 

        private readonly List<int> myPrefabInstanceHierarchies = new List<int>();

        public bool IsScene { get; internal set; }

        public AssetDocumentHierarchyElementContainer AssetDocumentHierarchyElementContainer { get; internal set; }
        
        public AssetDocumentHierarchyElement() : this(0, 0, 0, 0, 0)
        {
        }

        private AssetDocumentHierarchyElement(int otherCount, int gameObjectsCount, int transformCount, int scriptCount, int componentsCount)
        {
            myOtherBoxedElements = new List<IHierarchyElement>(otherCount);
            myTransformElements = new List<TransformHierarchy>(transformCount);
            myComponentElements = new List<ComponentHierarchy>(componentsCount);
            myGameObjectHierarchies = new List<GameObjectHierarchy>(gameObjectsCount);
            myScriptComponentElements = new List<ScriptComponentHierarchy>(scriptCount);
            myGameObjectLocationToTransform = new Dictionary<long, int>(transformCount);
            myOtherFakeStrippedElements = new List<IHierarchyElement>();
        }

        public string ContainerId => nameof(AssetDocumentHierarchyElementContainer);
        public void AddData(object data)
        {
            if (data == null)
                return;
            
            // avoid boxing
            if (data is GameObjectHierarchy gameObjectHierarchy)
                myGameObjectHierarchies.Add(gameObjectHierarchy);
            else if (data is ScriptComponentHierarchy scriptComponentHierarchy)
                myScriptComponentElements.Add(scriptComponentHierarchy);
            else if (data is TransformHierarchy transformHierarchy)
                myTransformElements.Add(transformHierarchy);
            else if (data is ComponentHierarchy componentHierarchy)
                myComponentElements.Add(componentHierarchy);
            else 
                myOtherBoxedElements.Add(data as IHierarchyElement);
        }

        public IHierarchyElement GetHierarchyElement(Guid? ownerGuid, long anchor, PrefabImportCache prefabImportCache)
        {
            Interruption.Current.CheckAndThrow();
            var result = SearchForAnchor(anchor);
            if (result != null)
            {
                if (!(result is IStrippedHierarchyElement) || prefabImportCache == null) // stipped means, that element is not real and we should import prefab
                    return result;
            }  
            
            // In prefabs files, anchor for stripped element is always generated by formula in PrefabsUtil. This means
            // that after we import elements from prefab file into another file, we could reuse anchor from stripped element to
            // get real element (in current implementation, imported objects are store in PrefabImportCache)
            // It is not true(!!!) for scene files, anchors for scene files could be generated by sequence generator. This means,
            // that anchor for stripped element could be '19' for example, but imported element will have another anchor.
            //
            // To unify all logic, if ownerGuid is related to scene file and achor points to stripped file, we will 
            // use new anchor which calculated in same way with prefab import  
            if (result != null && IsScene && result is IStrippedHierarchyElement strippedHierarchyElement )
            {
                var prefabInstance = strippedHierarchyElement.PrefabInstance;
                var correspondingObject = strippedHierarchyElement.CorrespondingSourceObject;
                anchor = PrefabsUtil.GetImportedDocumentAnchor(prefabInstance.LocalDocumentAnchor, correspondingObject.LocalDocumentAnchor);
            }

            if (prefabImportCache != null && ownerGuid != null)
            {
                Interruption.Current.CheckAndThrow();
                
                var elements = prefabImportCache.GetImportedElementsFor(ownerGuid.Value, this);
                
                if (elements.TryGetValue(anchor, out var importedResult))
                    return importedResult;
            }
            
            return null;
        }

        // boxing is not problem here
        private IHierarchyElement SearchForAnchor(long anchor)
        {
            return
                SearchForAnchor(myGameObjectHierarchies, anchor) ??
                SearchForAnchor(myTransformElements, anchor) ??
                SearchForAnchor(myScriptComponentElements, anchor) ??
                SearchForAnchor(myComponentElements, anchor) ??
                SearchForAnchor(myOtherBoxedElements, anchor) ??
                SearchForAnchor(myOtherFakeStrippedElements, anchor);
        }


        private IHierarchyElement SearchForAnchor<T>(List<T> elements, long anchor) where T : IHierarchyElement
        {
            var searchResult = elements.BinarySearchEx(a => a.Location.LocalDocumentAnchor.CompareTo(anchor));
            if (searchResult.IsHit)
                return searchResult.HitItem;

            return null;
        }
        

        public IEnumerable<IPrefabInstanceHierarchy> GetPrefabInstanceHierarchies()
        {
            for (int i = 0; i < myPrefabInstanceHierarchies.Count; i++)
            {
                var element = GetElementByInternalIndex(myPrefabInstanceHierarchies[i]);
                if (element != null)
                    Assertion.Assert(element is IPrefabInstanceHierarchy, "element is IPrefabInstanceHierarchy");
                yield return element as IPrefabInstanceHierarchy;
            }
        }

        private readonly object myLockObject = new object();
        private volatile bool myIsRestored = false;
        public void RestoreHierarchy(AssetDocumentHierarchyElementContainer hierarchyElementContainer, IPsiSourceFile sourceFile)
        {
            if (myIsRestored)
                return;
            
            lock (myLockObject)
            {
                if (myIsRestored)
                    return;
                
                AssetDocumentHierarchyElementContainer = hierarchyElementContainer;
                IsScene = sourceFile.GetLocation().ExtensionWithDot.Equals(UnityYamlConstants.Scene);

 
                var offset = 0;
                // concating arrays to one by index. see GetElementByInternalIndex too
                PrepareElements(myOtherBoxedElements, offset);
                offset += myOtherBoxedElements.Count;
                
                PrepareElements(myTransformElements, offset);
                offset += myTransformElements.Count;

                PrepareElements(myGameObjectHierarchies, offset);
                offset += myGameObjectHierarchies.Count;

                PrepareElements(myComponentElements, offset);
                offset += myComponentElements.Count;
                
                PrepareElements(myScriptComponentElements, offset);
                offset += myScriptComponentElements.Count;

                foreach (var prefabInstanceHierarchy in GetPrefabInstanceHierarchies())
                {
                    var correspondingSourceObjects = new HashSet<ExternalReference>();
                    foreach (var modification in prefabInstanceHierarchy.PrefabModifications)
                    {
                        var target = modification.Target;
                        if (!(target is ExternalReference externalReference))
                            continue;
                        correspondingSourceObjects.Add(externalReference);
                    }

                    foreach (var correspondingSourceObject in correspondingSourceObjects)
                    {
                        var fakeAnchor = PrefabsUtil.GetImportedDocumentAnchor(prefabInstanceHierarchy.Location.LocalDocumentAnchor, correspondingSourceObject.LocalDocumentAnchor);
                        myOtherFakeStrippedElements.Add(new StrippedHierarchyElement(
                            new LocalReference(sourceFile.PsiStorage.PersistentIndex.NotNull("owningPsiPersistentIndex != null"), fakeAnchor),
                            prefabInstanceHierarchy.Location, correspondingSourceObject));
                    }
                }
                
                myIsRestored = true;
            }
        }


        private void PrepareElements<T>(List<T> list, int curOffset) where  T : IHierarchyElement
        {
            list.Sort((a, b) => a.Location.LocalDocumentAnchor.CompareTo(b.Location.LocalDocumentAnchor));
            
            for (int i = 0; i < list.Count; i++)
            {
                var element = list[i];
                if (element is ITransformHierarchy transformHierarchy)
                {
                    var reference = transformHierarchy.OwningGameObject;
                    myGameObjectLocationToTransform[reference.LocalDocumentAnchor] = curOffset + i;
                }

                if (element is IPrefabInstanceHierarchy prefabInstanceHierarchy)
                    myPrefabInstanceHierarchies.Add(curOffset + i);
            }
        }

        internal ITransformHierarchy GetTransformHierarchy(GameObjectHierarchy gameObjectHierarchy)
        {
            var transformIndex = myGameObjectLocationToTransform.GetValueSafe(gameObjectHierarchy.Location.LocalDocumentAnchor, -1);
            if (transformIndex == -1)
                return null;

            var element = GetElementByInternalIndex(transformIndex);
            if (element != null)
                Assertion.Assert(element is ITransformHierarchy, "element is ITransformHierarchy");
            
            return element as ITransformHierarchy;
        }

        private IHierarchyElement GetElementByInternalIndex(int index)
        {
            if (index < myOtherBoxedElements.Count)
                return myOtherBoxedElements[index];

            index -= myOtherBoxedElements.Count;
            
            if (index < myTransformElements.Count)
                return myTransformElements[index];

            index -= myTransformElements.Count;
            
            if (index < myGameObjectHierarchies.Count)
                return myGameObjectHierarchies[index];

            index -= myGameObjectHierarchies.Count;
            
            if (index < myComponentElements.Count)
                return myComponentElements[index];

            index -= myComponentElements.Count;
            
            if (index < myScriptComponentElements.Count)
                return myScriptComponentElements[index];

            index -= myScriptComponentElements.Count;
            
            if (index < myOtherFakeStrippedElements.Count)
                return myOtherFakeStrippedElements[index];

            index -= myOtherFakeStrippedElements.Count;
            
            
            throw new IndexOutOfRangeException("Index was out of range in concated array");
        }

        public IEnumerable<IHierarchyElement> Elements()
        {
            foreach (var otherElement in myOtherBoxedElements)
                yield return otherElement;
            
            foreach (var otherElement in myTransformElements)
                yield return otherElement;
            
            foreach (var otherElement in myGameObjectHierarchies)
                yield return otherElement;
            
            foreach (var otherElement in myComponentElements)
                yield return otherElement;
            
            foreach (var otherElement in myScriptComponentElements)
                yield return otherElement;
            
            foreach (var fakeStrippedElement in myOtherFakeStrippedElements)
                yield return fakeStrippedElement;
        }
    }
}